'''Grade Predictor by Numpy
Author: Fei Zhao
'''
import numpy as np
import matplotlib.pyplot as plt

def load_data():
    '''数据生成及预处理'''
    # 使用随机数模拟制造数据集
    N = 1000    # 数据集中样本数量
    # 每个样本包括4项，其中前面3项是各考核项目得分，第4项是相应的学生的总成绩
    feature_names = [ 'HP', 'RT', 'RC', 'G']
    feature_num = len(feature_names)

    # 为了保持程序每次运行结果的一致性，此处设置固定的随机数种子
    np.random.seed(0)
    # 假设每个项目成绩的服从均值为85，标准差为10的正态分布
    data = 10*np.random.randn(N, feature_num)+85
    data[data<0] = 0
    data[data>100] = 100
    
    # 计算总成绩
    data[:, -1] =  0.40 * data[:, 0] + 0.12 * data[:, 1] + 0.48 * data[:, 2]

    # 将原数据集拆分成训练集和测试集
    # 这里使用80%的数据做训练，20%的数据做测试
    # 测试集和训练集必须是没有交集的
    ratio = 0.8
    offset = int(data.shape[0] * ratio)
    training_data = data[:offset]
    test_data = data[offset:]

    # 绘图显示制造的数据集中的总成绩分布
    fig = plt.figure(figsize=(10,4))
    ax1 = fig.add_subplot(121)
    plt.hist(training_data[:, -1], bins=10, density=True, rwidth=0.9)
    plt.xlabel('Scores')
    plt.ylabel('Proportion')
    ax1.set_title('Grades distribution of training dataset')
    ax2 = fig.add_subplot(122)
    plt.hist(test_data[:, -1], bins=10, density=True, rwidth=0.9)
    plt.xlabel('Scores')
    plt.ylabel('Proportion')
    ax2.set_title('Grades distribution of test dataset')
    plt.show()

    # 计算训练集的最大值，最小值
    maximums, minimums = training_data.max(axis=0), training_data.min(axis=0)
    
    # 对数据进行归一化处理
    for i in range(feature_num-1):
        data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
        
    data[:, -1] =  0.40 * data[:, 0] + 0.12 * data[:, 1] + 0.48 * data[:, 2]

    # 训练集和测试集的划分比例
    training_data = data[:offset]
    test_data = data[offset:]
    return training_data, test_data

    # 定义网络模型
class GPNet(object):
    '''
    Grade Predictor Network
    '''
    def __init__(self, num_of_weights):
        '''初始化网络参数'''
        # 为了保持程序每次运行结果的一致性，此处设置固定的随机数种子
        np.random.seed(0)
        # 随机产生w的初始值
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        '''前向计算'''
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        '''损失函数'''
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        '''计算梯度'''
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)        
        return gradient_w, gradient_b
    
    def update(self, gradient_w, gradient_b, eta = 0.01):
        '''更新参数'''
        self.w = self.w - eta * gradient_w
        self.b = self.b - eta * gradient_b
        
    def train(self, x, y, iterations=100, eta=0.01):
        '''训练网络'''
        losses = []
        for i in range(iterations):
            z = self.forward(x)
            L = self.loss(z, y)
            gradient_w, gradient_b = self.gradient(x, y)
            self.update(gradient_w, gradient_b, eta)
            losses.append(L)
            if (i+1) % 1000 == 0:
                print('iter {}, loss {}'.format(i, L))
        return losses
    
    def eval(self, x, y, precision=0.01):
        '''评估网络'''
        z = self.forward(x)
        error = np.abs(z - y)
        accuracy = np.sum(error < precision) / error.shape[0]
        return accuracy

    def predict(self, x):
        '''前向预测'''
        return self.forward(x)
    
    def parameters(self):
        '''获取模型参数'''
        return self.w, self.b

# 训练网络模型
# 获取数据
train_data, test_data = load_data()
x = train_data[:, :-1]
y = train_data[:, -1:]
# 创建网络
net = GPNet(3)
num_iterations=30000
# 启动训练
losses = net.train(x,y, iterations=num_iterations, eta=0.01)

# 画出损失函数的变化趋势
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.xlabel('iteration')
plt.ylabel('cost')
plt.show()

w, b = net.parameters()
print('----训练得到的网络模型参数----')
print('w: \n', w)
print('b: ', b)

# 评估网络准确度
accuracy = net.eval(test_data[:, :-1], test_data[:, -1:], 0.001)
print('测试网络模型准确度accuracy：', accuracy)

# 某同学平时得分85，大报告老师评分92，学生评分94
x = np.array([[85, 92, 94]])
grade = np.dot(x, np.array([0.40, 0.12, 0.48]))
predict_grade = net.predict(x)
print('同学平时得分85，大报告老师评分92，学生评分94：')
print('实际应得分：', np.around(grade, 2))
print('预测得分：', np.around(predict_grade, 2))